/*
Copyright 2009 Jeff Carnes

This file is part of Fluant.

Fluant is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

Fluant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.amiasleep.fluant.jsfl;

import java.io.*;
import java.awt.AWTException;
import java.awt.Robot;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;


public class JSFLRunner {
	
	protected final static int DEFAULT_TIMEOUT = 30000;
	protected final static int THREAD_DURATION = 1000;
	private final static String errorString = "**Error**";
	
	protected final String templatePath = "/";
	protected volatile File jsflFile;
	protected File workingFile;
	protected File outputFile;
	protected int timeout = DEFAULT_TIMEOUT;
	public boolean closeFlashWhenFinished = false;
	public boolean hideFlashWhenFinished = true;
	
	public Vector<String> errors = new Vector<String>();
	public Vector<String> warnings = new Vector<String>();
	public boolean failOnWarning = false;
	
	
	
	public JSFLRunner() throws CompatibilityException
	{
		if(!isCompatibleOS())
		{
			throw new CompatibilityException("Your operating system does not appear to be compatible with the Flash IDE");
		}
	}
	
	
	
	public JSFLRunner(File jsflFile) throws CompatibilityException
	{
		this(jsflFile, DEFAULT_TIMEOUT);
	}
	
	
	
	public JSFLRunner(File jsflFile, int timeout) throws CompatibilityException
	{
		if(!isCompatibleOS())
		{
			throw new CompatibilityException("Your operating system does not appear to be compatible with the Flash IDE");
		}
		
		this.jsflFile = jsflFile;
		this.timeout = timeout;
	}
	
	

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		File file = new File(args[0]);
		try
		{
			JSFLRunner runner;
			for(int i = 0; i < args.length; i++)
			{
				runner = new JSFLRunner(file);
				runner.run();
			}
		}
		catch(Exception ex)
		{
			System.out.println(ex.toString());
		}
	}
	
	
	
	public synchronized void run() throws JSFLExecuteException
	{
		if(jsflFile == null || !jsflFile.exists())
		{
			String message;
			if(jsflFile == null)
			{
				message = "No JSFL file specified.";
			}
			else
			{
				message = "Specified JSFL, " + jsflFile.getAbsolutePath() + ", does not exist.";
			}
			throw new JSFLExecuteException(message);
		}
		
		
		File runner = null;
		try
		{
			runner = getRunnerScript();
		}
		catch(Exception ex)
		{
			throw new JSFLExecuteException("Unable to create temporary JSFL file", ex);
		}
		
		
		String executable = getCommandString(runner);
		//System.out.println("Command: " + executable);
		try
		{
			Process p = Runtime.getRuntime().exec(executable, null, jsflFile.getParentFile());
			// TODO: Report errors from the execution error stream
			//InputStream stderr = p.getErrorStream();
			//InputStreamReader isr = new InputStreamReader(stderr);
		}
        catch (IOException ie) {
        	throw new JSFLExecuteException("Unable to execute JSFL in Flash IDE", ie);
        }
        catch (SecurityException se) {
        	throw new JSFLExecuteException("No permission to create a subprocess from which to publish the FLA", se);
        }
        
        
        Date start = new Date();
        Date now;
        while (!taskFinished()) {
        	now = new Date();
        	if( (now.getTime() - start.getTime()) > timeout)
        	{
        		throw new JSFLExecuteException("JSFL has run for longer than " + timeout + "ms. Execution may have failed.");
        	}
        	
        	// Every iteration will print a "." to show progress
            System.out.print(".");
            
            try {
                Thread.sleep(THREAD_DURATION);
            } catch (InterruptedException ex) {
            	throw new JSFLExecuteException(ex);
            }
        }
        
        System.out.println("");
        logOutput();
        
        
        // Only hide Flash if it's not set to close - it would already be
        // close by now, hence we'd end up hiding another app.
        if(!closeFlashWhenFinished && hideFlashWhenFinished)
        {
	        hideFlash();
        }
        
        
        boolean failWithWarnings = failOnWarning && warnings.size() > 0;
        boolean failWithErrors = errors.size() > 0;
        
        if(failWithWarnings || failWithErrors)
        {
	        StringBuilder errorOutputBuilder = new StringBuilder();
	        
	        if(failOnWarning)
	        {
				Iterator<String> warningsIter = warnings.iterator();
				while(warningsIter.hasNext())
				{
					errorOutputBuilder.append("WARNING: ");
					errorOutputBuilder.append(warningsIter.next());
					errorOutputBuilder.append("\n");
				}
	        }
	        
			Iterator<String> iterator = errors.iterator();
			while(iterator.hasNext())
			{
				errorOutputBuilder.append("ERROR: ");
				errorOutputBuilder.append(iterator.next());
				errorOutputBuilder.append("\n");
			}
			
			throw new JSFLExecuteException("There were errors executing this JSFL\n" + errorOutputBuilder.toString());
        }
	}
	
	
	
	protected File getRunnerScript() throws IOException, TemplateException, URISyntaxException
	{
		// Setup FreeMarker objects
		Configuration cfg = new Configuration();
		cfg.setClassForTemplateLoading(JSFLRunner.class, templatePath);
		Template template = cfg.getTemplate("run.ftl");
		Map<String,Object> root = new HashMap<String,Object>();
		
		// Create a URI with an empty authority to get the property formatting for Flash (file:///)
		URI uri = jsflFile.toURI();
		root.put("jsflFile", uri.toString().replace("file:/", "file:///"));
		
		workingFile = File.createTempFile("fluant", ".working");
		workingFile.deleteOnExit();
		// Create a URI with an empty authority to get the property formatting for Flash (file:///)
		URI finishedUri = workingFile.toURI();
		String finishedPath = finishedUri.toString().replace("file:/", "file:///");
		root.put("workingFile", finishedPath);
		
		outputFile = File.createTempFile("fluant", ".output");
		outputFile.deleteOnExit();
		// Create a URI with an empty authority to get the property formatting for Flash (file:///)
		URI outputUri = outputFile.toURI();
		String outputPath = outputUri.toString().replace("file:/", "file:///");
		root.put("outputFile", outputPath);
		
		root.put("closeWhenFinished", Boolean.valueOf(closeFlashWhenFinished).toString());
		
		
		// Try writing a JSFL file from the template
		File tempJSFL = File.createTempFile("fluant", ".jsfl");
		tempJSFL.deleteOnExit();
		Writer out = null;
		try
		{
			out = new FileWriter(tempJSFL);
			template.process(root, out);
			out.flush();
		}
		catch(IOException ioEx)
		{
			// We're just rethrowing, but the finally clause will make sure we try to close the File
			throw ioEx;
		}
		finally
		{
			out.close();
		}
		
		return tempJSFL;
	}
	
	
	
	private boolean taskFinished()
	{
		boolean isDone = !(workingFile != null && workingFile.exists());
		return isDone;
	}
	
	
	
	private String getCommandString(File jsflToRun)
	{
		String executable = null;
		if(isWindows())
		{
			executable = "cmd.exe /c start " + jsflToRun.getAbsolutePath();
		}
		else if(isMac())
		{
			executable = "open " + jsflToRun.getPath() + "";
		}
		
		return executable;
	}
	
	
	
	private boolean isCompatibleOS()
	{
		boolean isValid = isWindows() || isMac();
		return isValid;
	}
	
	
	
	private boolean isWindows()
	{
		String osName = System.getProperty("os.name").toLowerCase(Locale.US);
		boolean isWin = osName.indexOf("windows") > -1;
		return isWin;
	}
	
	
	
	private Boolean isMac()
	{
		String osName = System.getProperty("os.name").toLowerCase(Locale.US);
		boolean isMac = osName.indexOf("mac") > -1;
		return isMac;
	}
	
	
	
	private void logOutput() throws JSFLExecuteException
	{
		BufferedReader reader = null;
		try
		{
			FileReader fr = new FileReader(outputFile);
			reader = new BufferedReader( fr );
			
			String line = reader.readLine();
			while( line != null )
			{
				if( line.startsWith(errorString) )
				{
					String error = line.replace(errorString, "");
					String errorLine = "ERROR: " + error;
					
					errors.add(error);
					System.out.println(errorLine);
				}
				else if( line.startsWith("WARNING: ") )
				{
					String warning = line.replace("WARNING: ", "");
					warnings.add(warning);
					System.out.println(line);
				}
				else
				{
					System.out.println(" " + line);
				}
				
				line = reader.readLine();
			}
		}
		catch(IOException ioEx)
		{
			System.out.println("Error logging output from FLA: " + ioEx.getMessage());
		}
		finally
		{
			if(reader != null)
			{
				try
				{
					reader.close();
				}
				catch(IOException closeIoEx)
				{
					System.out.println("JSFLRunner.logOutput(): Unable to close BufferedReader");
				}
			}
		}
	}
	
	
	
	protected void hideFlash()
	{
		if(isMac())
		{
			try
	        {
		        Robot r = new Robot();
		        r.keyPress(KeyEvent.VK_META);
		        r.keyPress(KeyEvent.VK_H);
		        r.keyRelease(KeyEvent.VK_META);
		        r.keyRelease(KeyEvent.VK_H);
	        }
	        catch(AWTException ae)
	        {
	        	System.out.println(ae.getMessage());
	        }
		}
	}

}
